iT邦幫忙

2023 iThome 鐵人賽

DAY 29
0

自定義套件除了前面所說的,可以設置行為以及事件之外

Ktor 還提供了設置套件參數的方式

根據官網,我們可以看到教學是先定義了參數類別

class PluginConfiguration {
    var headerName: String = "Custom-Header-Name"
    var headerValue: String = "Default value"
}

然後我們就可以在套件內使用

val CustomHeaderPlugin = createApplicationPlugin(
    name = "CustomHeaderPlugin",
    createConfiguration = ::PluginConfiguration
) {
    val headerName = pluginConfig.headerName
    val headerValue = pluginConfig.headerValue
    pluginConfig.apply {
        onCall { call ->
            call.response.headers.append(headerName, headerValue)
        }
    }
}

我們再次看看 createApplicationPlugin 的簽名

public fun <PluginConfigT : Any> createApplicationPlugin(
    name: String,
    createConfiguration: () -> PluginConfigT,
    body: PluginBuilder<PluginConfigT>.() -> Unit
): ApplicationPlugin<PluginConfigT>

這邊我們可以看到 createConfiguration 的型態為 () -> PluginConfigT

由於 Ktor 事先不知道我們所定義的 PluginConfiguration 類別

因此這邊採用泛型的方式,來定義 createConfiguration 的回傳型態

另外這邊並不是直接送入一個實體物件,而是送入該類別的 callable references

所以寫法是

createConfiguration = ::PluginConfiguration

參考官網 Constructor references 的教學,這樣寫可以直接送入一個
() -> PluginConfiguration 的函數。

這樣一來,我們就可以在安裝套件時,調整對應的參數設置

install(CustomHeaderPlugin) {
    headerName = "X-Custom-Header"
    headerValue = "Hello, world!"
}

我們來看看 PluginConfiguration 這個物件,

是怎麼被送入我們自定義 CustomHeaderPlugin 內的。

首先,我們重新看 createApplicationPlugin 的定義

public fun <PluginConfigT : Any> createApplicationPlugin(
    name: String,
    createConfiguration: () -> PluginConfigT,
    body: PluginBuilder<PluginConfigT>.() -> Unit
): ApplicationPlugin<PluginConfigT> = object : ApplicationPlugin<PluginConfigT> {
    override val key: AttributeKey<PluginInstance> = AttributeKey(name)

    override fun install(
        pipeline: Application,
        configure: PluginConfigT.() -> Unit
    ): PluginInstance {
        return createPluginInstance(pipeline, pipeline, body, createConfiguration, configure)
    }
}

這邊使用了 Kotlin 的 object 關鍵字宣告了一個匿名類別,這邊會直接變成 ApplicationPlugin 的建構函數

ApplicationPlugin 定義如下

/**
 * Defines a [Plugin](https://ktor.io/docs/plugins.html) that is installed into Application.
 * @param TConfiguration is the configuration object type for this Plugin
 */
public interface ApplicationPlugin<out TConfiguration : Any> :
    BaseApplicationPlugin<Application, TConfiguration, PluginInstance>

BaseApplicationPlugin

/**
 * Defines a [Plugin](https://ktor.io/docs/plugins.html) that is installed into Application.
 * @param TPipeline is the type of the pipeline this plugin is compatible with
 * @param TConfiguration is the configuration object type for this Plugin
 * @param TPlugin is the instance type of the Plugin object
 */
public interface BaseApplicationPlugin<
    in TPipeline : Pipeline<*, ApplicationCall>,
    out TConfiguration : Any,
    TPlugin : Any
    > : Plugin<TPipeline, TConfiguration, TPlugin>

Plugin 則是

/**
 * Defines an installable [Plugin](https://ktor.io/docs/plugins.html).
 * @param TPipeline is the type of the pipeline this plugin is compatible with
 * @param TConfiguration is the configuration object type for this Plugin
 * @param TPlugin is the instance type of the Plugin object
 */
@Suppress("AddVarianceModifier")
public interface Plugin<
    in TPipeline : Pipeline<*, ApplicationCall>,
    out TConfiguration : Any,
    TPlugin : Any
    > {
    /**
     * A unique key that identifies a plugin.
     */
    public val key: AttributeKey<TPlugin>

    /**
     * A plugin's installation script.
     */
    public fun install(pipeline: TPipeline, configure: TConfiguration.() -> Unit): TPlugin
}

看完上面的程式,可能會讓人好奇,這樣的設計目的是什麼?又有什麼好處?

我們看看有哪些地方實作了 Plugin 這個介面

除了剛剛看過的 BaseApplicationPlugin

還會找到 BaseRouteScopedPlugin

public interface BaseRouteScopedPlugin<TConfiguration : Any, TPlugin : Any> :
    Plugin<ApplicationCallPipeline, TConfiguration, TPlugin>

另外,實作 BaseApplicationPlugin 介面的地方

除了 ApplicationPlugin

還有 DataConversion

/**
 * Object for installing [io.ktor.util.converters.DataConversion] plugin
 */
public object DataConversion :
    BaseApplicationPlugin<ApplicationCallPipeline, DataConversion.Configuration, DataConversion> {

    override fun install(
        pipeline: ApplicationCallPipeline,
        configure: DataConversion.Configuration.() -> Unit
    ): DataConversion {
        val configuration = DataConversion.Configuration().apply(configure)
        return DataConversion(configuration)
    }

    override val key: AttributeKey<DataConversion> = AttributeKey("DataConversion")
}

EnginePlugin

/**
 * A plugin to install into an engine pipeline.
 */
public object EnginePlugin : BaseApplicationPlugin<EnginePipeline, Config, ShutDownUrl> {
	override val key: AttributeKey<ShutDownUrl> = AttributeKey<ShutDownUrl>("shutdown.url")

	override fun install(pipeline: EnginePipeline, configure: Config.() -> Unit): ShutDownUrl {
		val config = Config()
		configure(config)

		val plugin = ShutDownUrl(config.shutDownUrl, config.exitCodeSupplier)
		pipeline.intercept(EnginePipeline.Before) {
			if (call.request.uri == plugin.url) {
				plugin.doShutdown(call)
			}
		}

		return plugin
	}
}

等等地方。

也就是說,透過定義較單純的介面,並讓介面之間相互實作

我們可以讓介面的用途和範圍變得更明確

也不需要大量撰寫相同定義的小介面

這可以讓我們的程式碼更加的簡潔,同時不犧牲太多的可讀性。

接著我們回到 createApplicationPlugin.install

override fun install(
	pipeline: Application,
	configure: PluginConfigT.() -> Unit
): PluginInstance {
	return createPluginInstance(pipeline, pipeline, body, createConfiguration, configure)
}

這邊的 createPluginInstance 定義如下

private fun <
    PipelineT : ApplicationCallPipeline,
    PluginConfigT : Any
    > Plugin<PipelineT, PluginConfigT, PluginInstance>.createPluginInstance(
    application: Application,
    pipeline: ApplicationCallPipeline,
    body: PluginBuilder<PluginConfigT>.() -> Unit,
    createConfiguration: () -> PluginConfigT,
    configure: PluginConfigT.() -> Unit
): PluginInstance {
    val config = createConfiguration().apply(configure)

    val currentPlugin = this
    val pluginBuilder = object : PluginBuilder<PluginConfigT>(currentPlugin.key) {
        override val application: Application = application
        override val pipeline: ApplicationCallPipeline = pipeline
        override val pluginConfig: PluginConfigT = config
    }

    pluginBuilder.setupPlugin(body)
    return PluginInstance(pluginBuilder)
}

到這邊,我們終於看到 createConfiguration 被呼叫並回傳 PluginConfigT 的步驟。

並且由於所有的函數都可以使用 apply 這個 scope function

所以我們可以利用 apply 來加上 configure 的內容。

並且在 pluginBuilder 內加上我們處理好的 pluginConfig

到這邊,我們就看過了自定義套件加上設置套件參數的撰寫方式,以及框架內如何實作。


上一篇
Day 28:自定義套件的 onCall、onCallReceive、onCallRespond
下一篇
Day 30:Ktor 原始碼的總結以及功能
系列文
深入解析 Kotlin 專案 Ktor 的程式碼,探索 Ktor 的強大功能30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言